package org.codefilarete.stalactite.spring.repository.query.nativ;

import java.sql.ResultSet;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.codefilarete.reflection.AccessorChain;
import org.codefilarete.stalactite.query.api.JoinLink;
import org.codefilarete.stalactite.query.model.Limit;
import org.codefilarete.stalactite.query.api.Selectable;
import org.codefilarete.stalactite.spring.repository.query.execution.AbstractQueryExecutor;
import org.codefilarete.stalactite.spring.repository.query.StalactiteQueryMethod;
import org.codefilarete.stalactite.spring.repository.query.execution.StalactiteQueryMethodInvocationParameters;
import org.codefilarete.stalactite.sql.ConnectionProvider;
import org.codefilarete.stalactite.sql.Dialect;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.result.Accumulator;
import org.codefilarete.stalactite.sql.result.ColumnedRow;
import org.codefilarete.stalactite.sql.result.ColumnedRowIterator;
import org.codefilarete.stalactite.sql.statement.ReadOperation;
import org.codefilarete.stalactite.sql.statement.SQLExecutionException;
import org.codefilarete.stalactite.sql.statement.StringParamedSQL;
import org.codefilarete.stalactite.sql.statement.binder.DefaultParameterBinders;
import org.codefilarete.stalactite.sql.statement.binder.PreparedStatementWriter;
import org.codefilarete.stalactite.sql.statement.binder.ResultSetReader;

import static org.codefilarete.stalactite.spring.repository.query.projection.PartTreeStalactiteProjection.buildHierarchicMap;

/**
 * Classes for cases where the SQL given by @{@link org.codefilarete.stalactite.spring.repository.query.NativeQuery} annotation is expected to give
 * a projection of domain entities. The result is expected to be a list of maps, one map per row to suit Spring Data way of creating projections.
 *
 * @author Guillaume Mary
 */
public class TupleNativeQueryExecutor extends AbstractQueryExecutor<List<Map<String, Object>>, Map<String, Object>> {
	
	private final IdentityHashMap<JoinLink<?, ?>, String> expectedAliasesInNativeQuery;
	private final IdentityHashMap<JoinLink<?, ?>, AccessorChain<?, ?>> columnToProperties;
	private final Supplier<Limit> limitSupplier;
	private final String sql;
	private final ConnectionProvider connectionProvider;
	private final Dialect dialect;
	
	public TupleNativeQueryExecutor(StalactiteQueryMethod method,
									String sql,
									Dialect dialect,
									ConnectionProvider connectionProvider,
									IdentityHashMap<? extends JoinLink<?, ?>, String> expectedAliasesInNativeQuery,
									IdentityHashMap<? extends JoinLink<?, ?>, ? extends AccessorChain<?, ?>> columnToProperties,
									Supplier<Limit> limitSupplier) {
		super(method);
		this.dialect = dialect;
		this.sql = sql;
		this.connectionProvider = connectionProvider;
		this.expectedAliasesInNativeQuery = (IdentityHashMap<JoinLink<?, ?>, String>) expectedAliasesInNativeQuery;
		this.columnToProperties = (IdentityHashMap<JoinLink<?, ?>, AccessorChain<?, ?>>) columnToProperties;
		this.limitSupplier = limitSupplier;
	}
	
	@Override
	public Supplier<List<Map<String, Object>>> buildQueryExecutor(StalactiteQueryMethodInvocationParameters invocationParameters) {
		return () -> {
			Map<String, Object> values = invocationParameters.getNamedValues();
			
			Map<String, PreparedStatementWriter<?>> parameterBinders = invocationParameters.bindParameters(dialect);
			String sqlToExecute = sql;
			// Taking pageable parameter into account: at first glance we could have thought that asking the user to add some "limit" and "offset"
			// clauses to its SQL was enough, but it's not due to the offset clause that is actually optional. Indeed, the offset is only required
			// for "next" pages, only the very first doesn't 
			if (invocationParameters.getParameters().hasPageableParameter()) {
				Limit limit = limitSupplier.get();
				if (limit != null) {
					sqlToExecute += " limit :limit";
					values.put("limit", limit.getCount());
					parameterBinders.put("limit", DefaultParameterBinders.INTEGER_BINDER);
					if (limit.getOffset() != null) {
						sqlToExecute += " offset :offset";
						values.put("offset", limit.getOffset());
						parameterBinders.put("offset", DefaultParameterBinders.INTEGER_BINDER);
					}
				}
			}
			
			StringParamedSQL statement = new StringParamedSQL(sqlToExecute, parameterBinders);
			statement.setValues(values);
			
			try (ReadOperation<String> readOperation = dialect.getReadOperationFactory().createInstance(statement, connectionProvider)) {
//						readOperation.setListener((SQLOperation.SQLOperationListener<ParamType>) operationListener);
				// Note that setValues must be done after operationListener set
				readOperation.setValues(statement.getValues());
				ResultSet resultSet = readOperation.execute();
				// NB: we give the same ParametersBinders of those given at ColumnParameterizedSelect since the row iterator is expected to read column from it
				Map<Selectable<?>, ResultSetReader<?>> columnReaders = new HashMap<>();
				expectedAliasesInNativeQuery.forEach((selectable, alias) -> {
					if (selectable instanceof Column) {
						columnReaders.put(selectable, dialect.getColumnBinderRegistry().getBinder((Column) selectable));
					} else {
						columnReaders.put(selectable, dialect.getColumnBinderRegistry().getBinder(selectable.getJavaType()));
					}
				});
				ColumnedRowIterator rowIterator = new ColumnedRowIterator(resultSet, columnReaders, expectedAliasesInNativeQuery);
				
				Accumulator<ColumnedRow, List<Map<String, Object>>, List<Map<String, Object>>> accumulator = new Accumulator<ColumnedRow, List<Map<String, Object>>, List<Map<String, Object>>>() {
					@Override
					public Supplier<List<Map<String, Object>>> supplier() {
						return LinkedList::new;
					}
					
					@Override
					public BiConsumer<List<Map<String, Object>>, ColumnedRow> aggregator() {
						return (finalResult, databaseRowDataProvider) -> {
							Map<String, Object> row = new HashMap<>();
							finalResult.add(row);
							for (Entry<JoinLink<?, ?>, AccessorChain<?, ?>> entry : columnToProperties.entrySet()) {
								buildHierarchicMap(entry.getValue(), databaseRowDataProvider.get(entry.getKey()), row);
							}
						};
					}
					
					@Override
					public Function<List<Map<String, Object>>, List<Map<String, Object>>> finisher() {
						return Function.identity();
					}
				};
				return accumulator.collect(() -> rowIterator);
			} catch (RuntimeException e) {
				throw new SQLExecutionException(statement.getSQL(), e);
			}
		};
	}
}
